export default function createTab({
  BaseComponent,
  CLASS_PREFIX,
  DATA_PREFIX_BASE,
  DATA_PREFIX,
  EventHandler,
  getNextActiveElement,
  isDisabled,
  SelectorEngine,
}) {
  /**
   * Constants
   */

  const NAME = 'tab'
  const DATA_KEY = `${DATA_PREFIX_BASE}.tab`
  const EVENT_KEY = `.${DATA_KEY}`

  const EVENT_HIDE = `hide${EVENT_KEY}`
  const EVENT_HIDDEN = `hidden${EVENT_KEY}`
  const EVENT_SHOW = `show${EVENT_KEY}`
  const EVENT_SHOWN = `shown${EVENT_KEY}`
  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`
  const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`

  const ARROW_LEFT_KEY = 'ArrowLeft'
  const ARROW_RIGHT_KEY = 'ArrowRight'
  const ARROW_UP_KEY = 'ArrowUp'
  const ARROW_DOWN_KEY = 'ArrowDown'
  const HOME_KEY = 'Home'
  const END_KEY = 'End'

  const CLASS_NAME_ACTIVE = `${CLASS_PREFIX}active`
  const CLASS_NAME_FADE = `${CLASS_PREFIX}fade`
  const CLASS_NAME_SHOW = `${CLASS_PREFIX}show`
  const CLASS_DROPDOWN = `${CLASS_PREFIX}dropdown`

  const SELECTOR_DROPDOWN_TOGGLE = `.${CLASS_PREFIX}dropdown-toggle`
  const SELECTOR_DROPDOWN_MENU = `.${CLASS_PREFIX}dropdown-menu`
  const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`

  const SELECTOR_TAB_PANEL = `.${CLASS_PREFIX}list-group, .${CLASS_PREFIX}nav, [role="tablist"]`
  const SELECTOR_OUTER = `.${CLASS_PREFIX}nav-item, .${CLASS_PREFIX}list-group-item`
  const SELECTOR_INNER = `.${CLASS_PREFIX}nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .${CLASS_PREFIX}list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`
  const SELECTOR_DATA_TOGGLE = `[data-${DATA_PREFIX}toggle="tab"], [data-${DATA_PREFIX}toggle="pill"], [data-${DATA_PREFIX}toggle="list"]` // TODO: could only be `tab` in v6
  const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`

  const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-${DATA_PREFIX}toggle="tab"], .${CLASS_NAME_ACTIVE}[data-${DATA_PREFIX}toggle="pill"], .${CLASS_NAME_ACTIVE}[data-${DATA_PREFIX}toggle="list"]`

  /**
   * Class definition
   */

  class Tab extends BaseComponent {
    constructor(element) {
      super(element)
      this._parent = this._element.closest(SELECTOR_TAB_PANEL)

      if (!this._parent) {
        return
        // TODO: should throw exception in v6
        // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)
      }

      // Set up initial aria attributes
      this._setInitialAttributes(this._parent, this._getChildren())

      EventHandler.on(this._element, EVENT_KEYDOWN, (event) =>
        this._keydown(event),
      )
    }

    // Getters
    static get NAME() {
      return NAME
    }

    // Public
    show() {
      // Shows this elem and deactivate the active sibling if exists
      const innerElem = this._element
      if (this._elemIsActive(innerElem)) {
        return
      }

      // Search for active tab on same parent to deactivate it
      const active = this._getActiveElem()

      const hideEvent = active
        ? EventHandler.trigger(active, EVENT_HIDE, { relatedTarget: innerElem })
        : null

      const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {
        relatedTarget: active,
      })

      if (
        showEvent.defaultPrevented ||
        (hideEvent && hideEvent.defaultPrevented)
      ) {
        return
      }

      this._deactivate(active, innerElem)
      this._activate(innerElem, active)
    }

    // Private
    _activate(element, relatedElem) {
      if (!element) {
        return
      }

      element.classList.add(CLASS_NAME_ACTIVE)

      this._activate(SelectorEngine.getElementFromSelector(element)) // Search and activate/show the proper section

      const complete = () => {
        if (element.getAttribute('role') !== 'tab') {
          element.classList.add(CLASS_NAME_SHOW)
          return
        }

        element.removeAttribute('tabindex')
        element.setAttribute('aria-selected', true)
        this._toggleDropDown(element, true)
        EventHandler.trigger(element, EVENT_SHOWN, {
          relatedTarget: relatedElem,
        })
      }

      this._queueCallback(
        complete,
        element,
        element.classList.contains(CLASS_NAME_FADE),
      )
    }

    _deactivate(element, relatedElem) {
      if (!element) {
        return
      }

      element.classList.remove(CLASS_NAME_ACTIVE)
      element.blur()

      this._deactivate(SelectorEngine.getElementFromSelector(element)) // Search and deactivate the shown section too

      const complete = () => {
        if (element.getAttribute('role') !== 'tab') {
          element.classList.remove(CLASS_NAME_SHOW)
          return
        }

        element.setAttribute('aria-selected', false)
        element.setAttribute('tabindex', '-1')
        this._toggleDropDown(element, false)
        EventHandler.trigger(element, EVENT_HIDDEN, {
          relatedTarget: relatedElem,
        })
      }

      this._queueCallback(
        complete,
        element,
        element.classList.contains(CLASS_NAME_FADE),
      )
    }

    _keydown(event) {
      if (
        ![
          ARROW_LEFT_KEY,
          ARROW_RIGHT_KEY,
          ARROW_UP_KEY,
          ARROW_DOWN_KEY,
          HOME_KEY,
          END_KEY,
        ].includes(event.key)
      ) {
        return
      }

      event.stopPropagation() // stopPropagation/preventDefault both added to support up/down keys without scrolling the page
      event.preventDefault()

      const children = this._getChildren().filter(
        (element) => !isDisabled(element),
      )
      let nextActiveElement

      if ([HOME_KEY, END_KEY].includes(event.key)) {
        nextActiveElement =
          children[event.key === HOME_KEY ? 0 : children.length - 1]
      } else {
        const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
        nextActiveElement = getNextActiveElement(
          children,
          event.target,
          isNext,
          true,
        )
      }

      if (nextActiveElement) {
        nextActiveElement.focus({ preventScroll: true })
        Tab.getOrCreateInstance(nextActiveElement).show()
      }
    }

    _getChildren() {
      // collection of inner elements
      return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent)
    }

    _getActiveElem() {
      return (
        this._getChildren().find((child) => this._elemIsActive(child)) || null
      )
    }

    _setInitialAttributes(parent, children) {
      this._setAttributeIfNotExists(parent, 'role', 'tablist')

      for (const child of children) {
        this._setInitialAttributesOnChild(child)
      }
    }

    _setInitialAttributesOnChild(child) {
      child = this._getInnerElement(child)
      const isActive = this._elemIsActive(child)
      const outerElem = this._getOuterElement(child)
      child.setAttribute('aria-selected', isActive)

      if (outerElem !== child) {
        this._setAttributeIfNotExists(outerElem, 'role', 'presentation')
      }

      if (!isActive) {
        child.setAttribute('tabindex', '-1')
      }

      this._setAttributeIfNotExists(child, 'role', 'tab')

      // set attributes to the related panel too
      this._setInitialAttributesOnTargetPanel(child)
    }

    _setInitialAttributesOnTargetPanel(child) {
      const target = SelectorEngine.getElementFromSelector(child)

      if (!target) {
        return
      }

      this._setAttributeIfNotExists(target, 'role', 'tabpanel')

      if (child.id) {
        this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`)
      }
    }

    _toggleDropDown(element, open) {
      const outerElem = this._getOuterElement(element)
      if (!outerElem.classList.contains(CLASS_DROPDOWN)) {
        return
      }

      const toggle = (selector, className) => {
        const element = SelectorEngine.findOne(selector, outerElem)
        if (element) {
          element.classList.toggle(className, open)
        }
      }

      toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE)
      toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW)
      outerElem.setAttribute('aria-expanded', open)
    }

    _setAttributeIfNotExists(element, attribute, value) {
      if (!element.hasAttribute(attribute)) {
        element.setAttribute(attribute, value)
      }
    }

    _elemIsActive(elem) {
      return elem.classList.contains(CLASS_NAME_ACTIVE)
    }

    // Try to get the inner element (usually the .nav-link)
    _getInnerElement(elem) {
      return elem.matches(SELECTOR_INNER_ELEM)
        ? elem
        : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem)
    }

    // Try to get the outer element (usually the .nav-item)
    _getOuterElement(elem) {
      return elem.closest(SELECTOR_OUTER) || elem
    }

    // Static
    static jQueryInterface(config) {
      return this.each(function () {
        const data = Tab.getOrCreateInstance(this)

        if (typeof config !== 'string') {
          return
        }

        if (
          data[config] === undefined ||
          config.startsWith('_') ||
          config === 'constructor'
        ) {
          throw new TypeError(`No method named "${config}"`)
        }

        data[config]()
      })
    }
  }

  /**
   * Data API implementation
   */

  EventHandler.on(
    document,
    EVENT_CLICK_DATA_API,
    SELECTOR_DATA_TOGGLE,
    function (event) {
      if (['A', 'AREA'].includes(this.tagName)) {
        event.preventDefault()
      }

      if (isDisabled(this)) {
        return
      }

      Tab.getOrCreateInstance(this).show()
    },
  )

  /**
   * Initialize on focus
   */
  EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
    for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {
      Tab.getOrCreateInstance(element)
    }
  })

  return Tab
}
